package main

import (
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"

	"github.com/BurntSushi/toml"
	"github.com/gorilla/mux"
)

// some const
const (
	UPOLAD_PACK          = "git-upload-pack"
	RECEIVE_PACK         = "git-receive-pack"
	CMD_UPLOAD_PACK      = "upload-pack"
	CMD_RECEIVE_PACK     = "receive-pack"
	CONTENT_TYPE_UPLOAD  = "application/x-git-upload-pack-advertisement"
	CONTENT_TYPE_RECEIVE = "application/x-git-receive-pack-advertisement"
)

// some pkt-line data
var PKT_UPLOAD_MSG = []byte("001e# service=git-upload-pack\n")
var PKT_RECEIVE_MSG = []byte("001f# service=git-receive-pack\n")
var PKT_FLUSH = []byte("0000")

// Multiple repos will be stored in this directory
var repoRealDir string

// global config
var config *ConfigVars

type ConfigVars struct {
	Address     string `toml:"address"`
	GitBin      string `toml:"git_bin"`
	RepoBaseDir string `toml:"repo_base_dir"`
}

type AccessContent struct {
	IsWire          bool
	IsPush          bool
	IsAdvertiseRefs bool
	RepoPath        string
}

func (ac *AccessContent) Render(w http.ResponseWriter, r *http.Request) {
	var args []string
	args = append(args, "-c", "transfer.hideRefs=refs/lightweight/", "-c", "transfer.hideRefs=refs/fetches/")

	if ac.IsPush {
		args = append(args, "-c", "transfer.hideRefs=refs/pull/", CMD_RECEIVE_PACK)
	} else {
		args = append(args, "-c", "uploadpack.allowfilter=1", CMD_UPLOAD_PACK)
	}
	args = append(args, "--stateless-rpc")
	if ac.IsAdvertiseRefs {
		ac.SendService(w)
		args = append(args, "--advertise-refs")
	}
	args = append(args, ac.RepoPath)

	cmd := exec.Command(config.GitBin, args...)

	envs := append(os.Environ(), "GIT_ROA_ENABLE=0")
	if ac.IsWire {
		envs = append(envs, "GIT_PROTOCOL=version=2")
	}

	var reader io.ReadCloser
	var err error
	switch r.Header.Get("Content-Encoding") {
	case "gzip":
		reader, err = gzip.NewReader(r.Body)
		if err != nil {
			RenderError(w, "build gzip reader err", 403)
			return
		}
	default:
		reader = r.Body
	}
	defer reader.Close()

	cmd.Stdout = w
	cmd.Stdin = reader
	cmd.Stderr = os.Stderr
	cmd.Env = envs

	if ac.IsPush {
		w.Header().Set("Content-Type", CONTENT_TYPE_RECEIVE)
	} else {
		w.Header().Set("Content-Type", CONTENT_TYPE_UPLOAD)
	}

	if err := cmd.Start(); err != nil {
		RenderError(w, "sub-process start err", 403)
		return
	}

	if err := cmd.Wait(); err != nil {
		RenderError(w, "wait sub-process err", 403)
		return
	}
}

func (ac *AccessContent) SendService(w http.ResponseWriter) {
	if ac.IsWire {
		return
	}

	var pktMsg []byte
	var contentType string
	if ac.IsPush {
		pktMsg, contentType = PKT_RECEIVE_MSG, CONTENT_TYPE_RECEIVE
	} else {
		pktMsg, contentType = PKT_UPLOAD_MSG, CONTENT_TYPE_UPLOAD
	}

	w.Header().Set("Content-Type", contentType)
	w.Write(pktMsg)
	w.Write(PKT_FLUSH)
}

func (ac *AccessContent) CreateRepo() error {
	exe, err := os.Executable()
	if err != nil {
		return err
	}
	basePath := filepath.Dir(exe)
	exampleRepoPath := filepath.Join(basePath, "repo.git.example")

	var args []string
	args = append(args, "-R", exampleRepoPath, ac.RepoPath)
	cmd := exec.Command("cp", args...)
	return cmd.Run()
}

func (ac *AccessContent) CheckRepoPath() error {
	fi, err := os.Stat(ac.RepoPath)
	if os.IsNotExist(err) && ac.IsPush {
		return ac.CreateRepo()
	}

	if err != nil {
		return err
	}

	if !fi.IsDir() {
		return errors.New("repo path is not dir")
	}
	return nil
}

func IsProtocolWire(r *http.Request) bool {
	version := r.Header.Get("Git-Protocol")
	return version == "version=2"
}

func RenderError(w http.ResponseWriter, msg string, status int) {
	w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
	w.WriteHeader(status)
	_, _ = w.Write([]byte(msg))
}

func JoinRepoPath(r *http.Request) string {
	vars := mux.Vars(r)
	return filepath.Join(repoRealDir, vars["repo"])
}

func GitRefHandler(w http.ResponseWriter, r *http.Request) {
	ac := &AccessContent{
		RepoPath:        JoinRepoPath(r),
		IsAdvertiseRefs: true,
		IsWire:          IsProtocolWire(r),
	}

	service := r.FormValue("service")

	switch service {
	case UPOLAD_PACK:
	case RECEIVE_PACK:
		ac.IsPush = true
	default:
		RenderError(w, "unknow service ["+service+"]", 400)
		return
	}

	if err := ac.CheckRepoPath(); err != nil {
		RenderError(w, err.Error(), 400)
		return
	}

	ac.Render(w, r)
}

func GitUploadHandler(w http.ResponseWriter, r *http.Request) {
	ac := &AccessContent{
		IsPush:   false,
		RepoPath: JoinRepoPath(r),
		IsWire:   IsProtocolWire(r),
	}

	ac.Render(w, r)
}

func GitReceiveHandler(w http.ResponseWriter, r *http.Request) {
	ac := &AccessContent{
		IsPush:   true,
		RepoPath: JoinRepoPath(r),
		IsWire:   IsProtocolWire(r),
	}

	ac.Render(w, r)
}

func NewRouter() *mux.Router {
	r := mux.NewRouter()
	r.HandleFunc("/{repo}/info/refs", GitRefHandler).Methods("GET")
	r.HandleFunc("/{repo}/git-upload-pack", GitUploadHandler).Methods("POST")
	r.HandleFunc("/{repo}/git-receive-pack", GitReceiveHandler).Methods("POST")
	return r
}

func Welcome() {
	fmt.Println("Usage repo dir:", repoRealDir)
	fmt.Println("Server will running on:", config.Address)
	fmt.Println("Have fun!")
}

func GetRepoDIr() (string, error) {
	exe, err := os.Executable()
	if err != nil {
		return "", err
	}
	basePath := filepath.Dir(exe)
	repoDir := filepath.Join(basePath, config.RepoBaseDir)
	fi, err := os.Stat(repoDir)
	if os.IsNotExist(err) {
		err := os.Mkdir(repoDir, 0755)
		return repoDir, err
	}

	if err != nil {
		return "", err
	}

	if !fi.IsDir() {
		return "", errors.New(repoDir + " is not a dir")
	}
	return repoDir, nil
}

func Dea(msg ...interface{}) {
	fmt.Println(msg...)
	os.Exit(1)
}

func InitConfig() {
	if len(os.Args) < 2 {
		Dea("missing config path from args")
	}
	configPath := os.Args[1]
	if _, err := toml.DecodeFile(configPath, &config); err != nil {
		Dea("decode config file err:", err.Error())
	}

	if config.Address == "" {
		config.Address = "0.0.0.0:8080"
	}

	if config.GitBin == "" {
		config.GitBin = "/usr/bin/git"
	}

	if config.RepoBaseDir == "" {
		config.RepoBaseDir = "repositories"
	}

	var err error
	repoRealDir, err = GetRepoDIr()
	if err != nil {
		Dea("build repo dir err:", err.Error())
	}
}

func init() {
	InitConfig()

	Welcome()
}

func main() {
	var srv http.Server
	srv.Addr = config.Address
	srv.Handler = NewRouter()

	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		fmt.Println("server err", err)
		return
	}
	fmt.Println("server closed!")
}
